Skip to content

Commit e70862b

Browse files
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 <[email protected]>
1 parent 7c30f61 commit e70862b

File tree

19 files changed

+152
-73
lines changed

19 files changed

+152
-73
lines changed

desktop/src/app.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use graph_craft::wasm_application_io::WasmApplicationIo;
1010
use graphene_std::Color;
1111
use graphene_std::raster::Image;
1212
use graphite_editor::application::Editor;
13-
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
1413
use graphite_editor::messages::prelude::*;
1514
use std::fs;
1615
use std::sync::Arc;
@@ -79,7 +78,8 @@ impl WinitApp {
7978
String::new()
8079
});
8180
let message = PortfolioMessage::OpenDocumentFile {
82-
document_name: path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
81+
document_name: None,
82+
document_path: Some(path),
8383
document_serialized_content: content,
8484
};
8585
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
@@ -294,7 +294,8 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
294294
let Some(content) = load_string(&path) else { return };
295295

296296
let message = PortfolioMessage::OpenDocumentFile {
297-
document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()),
297+
document_name: None,
298+
document_path: Some(path),
298299
document_serialized_content: content,
299300
};
300301
self.dispatch_message(message.into());

editor/src/consts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
150150
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
151151

152152
// DOCUMENT
153+
pub const FILE_EXTENSION: &str = "graphite";
153154
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
154-
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
155155
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
156156
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
157157

editor/src/dispatcher.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,8 @@ mod test {
497497
);
498498

499499
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
500-
document_name: document_name.into(),
500+
document_name: Some(document_name.to_string()),
501+
document_path: None,
501502
document_serialized_content,
502503
});
503504

editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
4444
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
4545

4646
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
47-
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
47+
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
4848
file_type: self.file_type,
4949
scale_factor: self.scale_factor,
5050
bounds: self.bounds,

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping {
340340
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
341341
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
342342
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
343+
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
343344
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
344345
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
345346
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),

editor/src/messages/portfolio/document/document_message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pub enum DocumentMessage {
118118
RenderRulers,
119119
RenderScrollbars,
120120
SaveDocument,
121+
SaveDocumentAs,
121122
SavedDocument {
122123
path: Option<PathBuf>,
123124
},

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO
66
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
77
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
88
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
9-
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
9+
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
1010
use crate::messages::input_mapper::utility_types::macros::action_keys;
1111
use crate::messages::layout::utility_types::widget_prelude::*;
1212
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
@@ -85,8 +85,6 @@ pub struct DocumentMessageHandler {
8585
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
8686
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
8787
pub collapsed: CollapsedLayers,
88-
/// The name of the document, which is displayed in the tab and title bar of the editor.
89-
pub name: String,
9088
/// The full Git commit hash of the Graphite repository that was used to build the editor.
9189
/// We save this to provide a hint about which version of the editor was used to create the document.
9290
pub commit_hash: String,
@@ -113,6 +111,12 @@ pub struct DocumentMessageHandler {
113111
// Fields omitted from the saved document format
114112
// =============================================
115113
//
114+
/// The name of the document, which is displayed in the tab and title bar of the editor.
115+
#[serde(skip)]
116+
pub name: String,
117+
/// The path of the to the document file.
118+
#[serde(skip)]
119+
pub(crate) path: Option<PathBuf>,
116120
/// 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
117121
#[serde(skip)]
118122
breadcrumb_network_path: Vec<NodeId>,
@@ -125,9 +129,6 @@ pub struct DocumentMessageHandler {
125129
/// Stack of document network snapshots for future history states.
126130
#[serde(skip)]
127131
document_redo_history: VecDeque<NodeNetworkInterface>,
128-
/// The path of the to the document file.
129-
#[serde(skip)]
130-
path: Option<PathBuf>,
131132
/// Hash of the document snapshot that was most recently saved to disk by the user.
132133
#[serde(skip)]
133134
saved_hash: Option<u64>,
@@ -159,7 +160,6 @@ impl Default for DocumentMessageHandler {
159160
// ============================================
160161
network_interface: default_document_network_interface(),
161162
collapsed: CollapsedLayers::default(),
162-
name: DEFAULT_DOCUMENT_NAME.to_string(),
163163
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
164164
document_ptz: PTZ::default(),
165165
document_mode: DocumentMode::DesignMode,
@@ -172,11 +172,12 @@ impl Default for DocumentMessageHandler {
172172
// =============================================
173173
// Fields omitted from the saved document format
174174
// =============================================
175+
name: DEFAULT_DOCUMENT_NAME.to_string(),
176+
path: None,
175177
breadcrumb_network_path: Vec::new(),
176178
selection_network_path: Vec::new(),
177179
document_undo_history: VecDeque::new(),
178180
document_redo_history: VecDeque::new(),
179-
path: None,
180181
saved_hash: None,
181182
auto_saved_hash: None,
182183
layer_range_selection_reference: None,
@@ -947,7 +948,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
947948
responses.add(OverlaysMessage::Draw);
948949
}
949950
DocumentMessage::RenameDocument { new_name } => {
950-
self.name = new_name;
951+
self.name = new_name.clone();
952+
953+
self.path = None;
954+
self.set_save_state(false);
955+
951956
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
952957
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
953958
}
@@ -1020,25 +1025,40 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
10201025
multiplier: scrollbar_multiplier.into(),
10211026
});
10221027
}
1023-
DocumentMessage::SaveDocument => {
1028+
DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => {
1029+
if let DocumentMessage::SaveDocumentAs = message {
1030+
self.path = None;
1031+
}
1032+
10241033
self.set_save_state(true);
10251034
responses.add(PortfolioMessage::AutoSaveActiveDocument);
10261035
// Update the save status of the just saved document
10271036
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
10281037

1029-
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
1030-
true => self.name.clone(),
1031-
false => self.name.clone() + FILE_SAVE_SUFFIX,
1032-
};
10331038
responses.add(FrontendMessage::TriggerSaveDocument {
10341039
document_id,
1035-
name,
1040+
name: format!("{}.{}", self.name.clone(), FILE_EXTENSION),
10361041
path: self.path.clone(),
10371042
content: self.serialize_document().into_bytes(),
10381043
})
10391044
}
10401045
DocumentMessage::SavedDocument { path } => {
10411046
self.path = path;
1047+
1048+
// Update the name to match the file stem
1049+
let document_name_from_path = self.path.as_ref().and_then(|path| {
1050+
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
1051+
path.file_stem().map(|n| n.to_string_lossy().to_string())
1052+
} else {
1053+
None
1054+
}
1055+
});
1056+
if let Some(name) = document_name_from_path {
1057+
self.name = name;
1058+
1059+
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
1060+
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
1061+
}
10421062
}
10431063
DocumentMessage::SelectParentLayer => {
10441064
let selected_nodes = self.network_interface.selected_nodes();
@@ -1571,6 +1591,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
15711591
ZoomCanvasToFitAll,
15721592
);
15731593

1594+
// Additional actions available on desktop
1595+
#[cfg(not(target_family = "wasm"))]
1596+
common.extend(actions!(DocumentMessageDiscriminant::SaveDocumentAs));
1597+
15741598
// Additional actions if there are any selected layers
15751599
if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() {
15761600
let mut select = actions!(DocumentMessageDiscriminant;

editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,25 @@ impl LayoutHolder for MenuBarMessageHandler {
101101
..MenuBarEntry::default()
102102
},
103103
],
104-
vec![MenuBarEntry {
105-
label: "Save".into(),
106-
icon: Some("Save".into()),
107-
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
108-
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
109-
disabled: no_active_document,
110-
..MenuBarEntry::default()
111-
}],
104+
vec![
105+
MenuBarEntry {
106+
label: "Save".into(),
107+
icon: Some("Save".into()),
108+
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
109+
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
110+
disabled: no_active_document,
111+
..MenuBarEntry::default()
112+
},
113+
#[cfg(not(target_family = "wasm"))]
114+
MenuBarEntry {
115+
label: "Save As…".into(),
116+
icon: Some("Save".into()),
117+
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs),
118+
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()),
119+
disabled: no_active_document,
120+
..MenuBarEntry::default()
121+
},
122+
],
112123
vec![
113124
MenuBarEntry {
114125
label: "Import…".into(),

editor/src/messages/portfolio/portfolio_message.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::messages::prelude::*;
66
use graphene_std::Color;
77
use graphene_std::raster::Image;
88
use graphene_std::text::Font;
9+
use std::path::PathBuf;
910

1011
#[impl_message(Message, Portfolio)]
1112
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -66,18 +67,20 @@ pub enum PortfolioMessage {
6667
NextDocument,
6768
OpenDocument,
6869
OpenDocumentFile {
69-
document_name: String,
70+
document_name: Option<String>,
71+
document_path: Option<PathBuf>,
7072
document_serialized_content: String,
7173
},
72-
ToggleResetNodesToDefinitionsOnOpen,
7374
OpenDocumentFileWithId {
7475
document_id: DocumentId,
75-
document_name: String,
76+
document_name: Option<String>,
77+
document_path: Option<PathBuf>,
7678
document_is_auto_saved: bool,
7779
document_is_saved: bool,
7880
document_serialized_content: String,
7981
to_front: bool,
8082
},
83+
ToggleResetNodesToDefinitionsOnOpen,
8184
PasteIntoFolder {
8285
clipboard: Clipboard,
8386
parent: LayerNodeIdentifier,
@@ -115,7 +118,7 @@ pub enum PortfolioMessage {
115118
document_id: DocumentId,
116119
},
117120
SubmitDocumentExport {
118-
file_name: String,
121+
name: String,
119122
file_type: FileType,
120123
scale_factor: f64,
121124
bounds: ExportBounds,

editor/src/messages/portfolio/portfolio_message_handler.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
22
use super::document::utility_types::network_interface;
33
use super::utility_types::{PanelType, PersistentData};
44
use crate::application::generate_uuid;
5-
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
5+
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
66
use crate::messages::animation::TimingInformation;
77
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
88
use crate::messages::dialog::simple_dialogs;
@@ -419,12 +419,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
419419
}
420420
PortfolioMessage::OpenDocumentFile {
421421
document_name,
422+
document_path,
422423
document_serialized_content,
423424
} => {
424425
let document_id = DocumentId(generate_uuid());
425426
responses.add(PortfolioMessage::OpenDocumentFileWithId {
426427
document_id,
427428
document_name,
429+
document_path,
428430
document_is_auto_saved: false,
429431
document_is_saved: true,
430432
document_serialized_content,
@@ -439,6 +441,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
439441
PortfolioMessage::OpenDocumentFileWithId {
440442
document_id,
441443
document_name,
444+
document_path,
442445
document_is_auto_saved,
443446
document_is_saved,
444447
document_serialized_content,
@@ -450,10 +453,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
450453
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
451454

452455
// Deserialize the document
453-
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
454-
document.name.clone_from(&document_name);
455-
document
456-
});
456+
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content);
457457

458458
// Display an error to the user if the document could not be opened
459459
let mut document = match document {
@@ -514,6 +514,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
514514
document.set_auto_save_state(document_is_auto_saved);
515515
document.set_save_state(document_is_saved);
516516

517+
let document_name_from_path = document_path.as_ref().and_then(|path| {
518+
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
519+
path.file_stem().map(|n| n.to_string_lossy().to_string())
520+
} else {
521+
None
522+
}
523+
});
524+
525+
match (document_name, document_path, document_name_from_path) {
526+
(Some(name), _, None) => {
527+
document.name = name;
528+
}
529+
(_, Some(path), Some(name)) => {
530+
document.name = name;
531+
document.path = Some(path);
532+
}
533+
(_, _, Some(name)) => {
534+
document.name = name;
535+
}
536+
_ => {
537+
document.name = DEFAULT_DOCUMENT_NAME.to_string();
538+
}
539+
}
540+
517541
// Load the document into the portfolio so it opens in the editor
518542
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
519543
}
@@ -899,15 +923,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
899923
}
900924
}
901925
PortfolioMessage::SubmitDocumentExport {
902-
file_name,
926+
name,
903927
file_type,
904928
scale_factor,
905929
bounds,
906930
transparent_background,
907931
} => {
908932
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
909933
let export_config = ExportConfig {
910-
file_name,
934+
name,
911935
file_type,
912936
scale_factor,
913937
bounds,

0 commit comments

Comments
 (0)