Allow editing plaintext Files
This commit is contained in:
+108
-21
@@ -79,9 +79,15 @@ impl App {
|
||||
|
||||
// -- file actions -----------------------------------------------------
|
||||
|
||||
/// Whether anything would be lost by replacing the open canvas: changes to
|
||||
/// the canvas itself, or to a file edited through one of its nodes.
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.doc.is_dirty() || self.view.files.is_dirty()
|
||||
}
|
||||
|
||||
/// Runs `pending` now, or asks about unsaved changes first.
|
||||
fn guard(&mut self, pending: Pending) -> bool {
|
||||
if self.doc.is_dirty() {
|
||||
if self.has_unsaved_changes() {
|
||||
self.pending = Some(pending);
|
||||
false
|
||||
} else {
|
||||
@@ -130,21 +136,52 @@ impl App {
|
||||
self.view.zoom_to_fit(&self.doc.canvas);
|
||||
}
|
||||
|
||||
/// Saves, asking for a location when the document has never been saved.
|
||||
/// Returns `true` when the file made it to disk.
|
||||
/// Saves the canvas, asking for a location when it has never been saved,
|
||||
/// and writes every file edited through a file node back to its own file.
|
||||
///
|
||||
/// Returns `true` when everything made it to disk.
|
||||
fn save(&mut self, ctx: &Context) -> bool {
|
||||
match self.doc.save() {
|
||||
Ok(true) => {
|
||||
let name = self.doc.display_name();
|
||||
self.set_status(format!("Saved {name}"), ctx);
|
||||
true
|
||||
}
|
||||
Ok(false) => self.save_as(ctx),
|
||||
Err(err) => {
|
||||
self.error = Some(err);
|
||||
false
|
||||
// A canvas that has never been saved is only asked for a location when
|
||||
// it has changes of its own: an edit made through a file node goes to
|
||||
// that file and needs nothing from the canvas.
|
||||
let canvas_pending = self.doc.is_dirty() || self.doc.path().is_some();
|
||||
let mut ok = true;
|
||||
let mut saved_canvas = false;
|
||||
if canvas_pending {
|
||||
match self.doc.save() {
|
||||
Ok(true) => saved_canvas = true,
|
||||
Ok(false) => {
|
||||
ok = self.save_as(ctx);
|
||||
saved_canvas = ok;
|
||||
}
|
||||
Err(err) => {
|
||||
self.error = Some(err);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The files second, so that one that will not open for writing cannot
|
||||
// keep the canvas from being saved.
|
||||
let (files, errors) = self.view.files.save_all();
|
||||
if !errors.is_empty() {
|
||||
self.error = Some(errors.join("\n\n"));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
let mut saved: Vec<String> = Vec::new();
|
||||
if saved_canvas {
|
||||
saved.push(self.doc.display_name());
|
||||
}
|
||||
match files {
|
||||
0 => {}
|
||||
1 => saved.push("1 file".to_owned()),
|
||||
n => saved.push(format!("{n} files")),
|
||||
}
|
||||
if !saved.is_empty() {
|
||||
self.set_status(format!("Saved {}", saved.join(" and ")), ctx);
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
fn save_as(&mut self, ctx: &Context) -> bool {
|
||||
@@ -540,7 +577,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn request_quit(&mut self, ctx: &Context) {
|
||||
if self.doc.is_dirty() {
|
||||
if self.has_unsaved_changes() {
|
||||
self.pending = Some(Pending::Quit);
|
||||
} else {
|
||||
self.quitting = true;
|
||||
@@ -624,9 +661,22 @@ impl App {
|
||||
self.save_as(ctx);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Reload file previews").clicked() {
|
||||
self.view.previews.clear();
|
||||
self.set_status("Reloaded previews", ctx);
|
||||
if ui
|
||||
.button("Reload files")
|
||||
.on_hover_text("Read the files behind file nodes again")
|
||||
.clicked()
|
||||
{
|
||||
// Files with edits that have not been saved yet are left
|
||||
// alone: reading over them would throw the edits away.
|
||||
let dropped = self.view.files.reload();
|
||||
let kept = self.view.files.dirty_count();
|
||||
self.set_status(
|
||||
match kept {
|
||||
0 => format!("Reloaded {dropped} file(s)"),
|
||||
n => format!("Reloaded {dropped} file(s), kept {n} with edits"),
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
@@ -836,6 +886,22 @@ impl App {
|
||||
);
|
||||
ui.separator();
|
||||
|
||||
// Files edited through their nodes are written by Save along with
|
||||
// the canvas, so say how many are waiting for it.
|
||||
let edited = self.view.files.dirty_count();
|
||||
if edited > 0 {
|
||||
ui.label(
|
||||
RichText::new(match edited {
|
||||
1 => "1 edited file".to_owned(),
|
||||
n => format!("{n} edited files"),
|
||||
})
|
||||
.size(12.0)
|
||||
.color(palette.accent),
|
||||
)
|
||||
.on_hover_text("Saving the canvas writes these back too");
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
if let Some((message, at)) = &self.status {
|
||||
if ctx.input(|i| i.time) - at < STATUS_SECONDS {
|
||||
ui.label(RichText::new(message).size(12.0).color(palette.text_muted));
|
||||
@@ -866,11 +932,28 @@ impl App {
|
||||
fn dialogs(&mut self, ctx: &Context) {
|
||||
if self.pending.is_some() {
|
||||
let name = self.doc.display_name();
|
||||
let edited = self.view.files.dirty_count();
|
||||
egui::Modal::new(egui::Id::new("unsaved")).show(ctx, |ui| {
|
||||
ui.set_width(360.0);
|
||||
ui.heading("Unsaved changes");
|
||||
ui.add_space(6.0);
|
||||
ui.label(format!("“{name}” has changes that are not saved yet."));
|
||||
let what = match (self.doc.is_dirty(), edited) {
|
||||
(true, 0) => format!("“{name}” has changes that are not saved yet."),
|
||||
(true, 1) => format!(
|
||||
"“{name}” and one of its files have changes that are not saved yet."
|
||||
),
|
||||
(true, n) => format!(
|
||||
"“{name}” and {n} of its files have changes that are not saved yet."
|
||||
),
|
||||
(false, 1) => {
|
||||
"A file edited on this canvas has changes that are not saved yet."
|
||||
.to_owned()
|
||||
}
|
||||
(false, n) => format!(
|
||||
"{n} files edited on this canvas have changes that are not saved yet."
|
||||
),
|
||||
};
|
||||
ui.label(what);
|
||||
ui.add_space(12.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save and continue").clicked() {
|
||||
@@ -951,7 +1034,11 @@ impl App {
|
||||
let title = format!(
|
||||
"{}{} — JSON Canvas Editor",
|
||||
self.doc.display_name(),
|
||||
if self.doc.is_dirty() { " •" } else { "" }
|
||||
if self.has_unsaved_changes() {
|
||||
" •"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
if title != self.title {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Title(title.clone()));
|
||||
@@ -976,7 +1063,7 @@ impl eframe::App for App {
|
||||
|
||||
// Closing the window with unsaved changes asks first.
|
||||
if ctx.input(|i| i.viewport().close_requested()) && !self.quitting {
|
||||
if self.doc.is_dirty() {
|
||||
if self.has_unsaved_changes() {
|
||||
ctx.send_viewport_cmd(ViewportCommand::CancelClose);
|
||||
self.pending = Some(Pending::Quit);
|
||||
} else {
|
||||
@@ -1045,7 +1132,7 @@ const SHORTCUTS: &[(&str, &str)] = &[
|
||||
("F", "Zoom to the selection"),
|
||||
("Escape", "Stop editing, clear the selection"),
|
||||
("Arrow keys", "Nudge (hold Shift for 10 units)"),
|
||||
("Double-click", "New note, or edit the one under the cursor"),
|
||||
("Double-click", "New note, or edit what is under the cursor"),
|
||||
("Middle / right drag", "Pan the canvas"),
|
||||
("Ctrl+wheel", "Zoom"),
|
||||
("Ctrl+0 / Ctrl+1", "Zoom to fit / actual size"),
|
||||
|
||||
Reference in New Issue
Block a user