'save as' feature for map edits. autosave otherwise.

This commit is contained in:
Dustin Carlino 2020-02-06 12:31:06 -08:00
parent 1147d29d98
commit 872cd0cba6
9 changed files with 81 additions and 65 deletions

View File

@ -345,3 +345,7 @@ pub fn basename(path: &str) -> String {
.into_string()
.unwrap()
}
pub fn file_exists(path: String) -> bool {
Path::new(&path).exists()
}

View File

@ -15,10 +15,10 @@ pub use crate::collections::{
};
pub use crate::error::Error;
pub use crate::io::{
basename, deserialize_btreemap, deserialize_multimap, find_next_file, find_prev_file,
list_all_objects, load_all_objects, maybe_read_binary, maybe_read_json, read_binary, read_json,
serialize_btreemap, serialize_multimap, serialized_size_bytes, to_json, write_binary,
write_json, FileWithProgress,
basename, deserialize_btreemap, deserialize_multimap, file_exists, find_next_file,
find_prev_file, list_all_objects, load_all_objects, maybe_read_binary, maybe_read_json,
read_binary, read_json, serialize_btreemap, serialize_multimap, serialized_size_bytes, to_json,
write_binary, write_json, FileWithProgress,
};
pub use crate::logs::Warn;
pub use crate::random::{fork_rng, WeightedUsizeChoice};

View File

@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.875 0.625H2.375C1.40375 0.625 0.625 1.4125 0.625 2.375V14.625C0.625 15.5875 1.40375 16.375 2.375 16.375H14.625C15.5875 16.375 16.375 15.5875 16.375 14.625V4.125L12.875 0.625ZM8.5 14.625C7.0475 14.625 5.875 13.4525 5.875 12C5.875 10.5475 7.0475 9.375 8.5 9.375C9.9525 9.375 11.125 10.5475 11.125 12C11.125 13.4525 9.9525 14.625 8.5 14.625ZM11.125 5.875H2.375V2.375H11.125V5.875Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -139,7 +139,6 @@ fn launch_test(test: &ABTest, ui: &mut UI, ctx: &mut EventCtx) -> ABTestMode {
ui,
MapEdits::load(&test.map_name, &test.edits1_name, &mut timer),
);
ui.primary.map.mark_edits_fresh();
ui.primary
.map
.recalculate_pathfinding_after_edits(&mut timer);
@ -193,7 +192,6 @@ fn launch_test(test: &ABTest, ui: &mut UI, ctx: &mut EventCtx) -> ABTestMode {
MapEdits::load(&test.map_name, &test.edits2_name, &mut timer),
);
std::mem::swap(&mut ui.primary, &mut secondary);
secondary.map.mark_edits_fresh();
secondary
.map
.recalculate_pathfinding_after_edits(&mut timer);

View File

@ -62,6 +62,10 @@ impl EditMode {
.recalculate_pathfinding_after_edits(&mut timer);
// Parking state might've changed
ui.primary.clear_sim();
// Autosave
if ui.primary.map.get_edits().edits_name != "no_edits" {
ui.primary.map.save_edits();
}
Transition::PopThenReplace(Box::new(SandboxMode::new(ctx, ui, self.mode.clone())))
})
}
@ -105,6 +109,10 @@ impl State for EditMode {
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"load edits" => {
// Autosave first
if ui.primary.map.get_edits().edits_name != "no_edits" {
ui.primary.map.save_edits();
}
return Transition::Push(make_load_edits(
self.composite.rect_of("load edits").clone(),
self.mode.clone(),
@ -113,9 +121,9 @@ impl State for EditMode {
"finish editing" => {
return self.quit(ctx, ui);
}
"save edits" => {
"save edits as" => {
return Transition::Push(WizardState::new(Box::new(|wiz, ctx, ui| {
save_edits(&mut wiz.wrap(ctx), ui)?;
save_edits_as(&mut wiz.wrap(ctx), ui)?;
Some(Transition::Pop)
})));
}
@ -198,13 +206,18 @@ impl State for EditMode {
}
}
pub fn save_edits(wizard: &mut WrappedWizard, ui: &mut UI) -> Option<()> {
pub fn save_edits_as(wizard: &mut WrappedWizard, ui: &mut UI) -> Option<()> {
let map = &mut ui.primary.map;
let new_default_name = if map.get_edits().edits_name == "no_edits" {
"untitled edits".to_string()
} else {
format!("copy of {}", map.get_edits().edits_name)
};
let rename = if map.get_edits().edits_name == "no_edits" {
Some(wizard.input_something(
"Name these map edits",
None,
let name = loop {
let candidate = wizard.input_something(
"Name the new copy of these edits",
Some(new_default_name.clone()),
Box::new(|l| {
if l.contains("/") || l == "no_edits" || l == "" {
None
@ -212,27 +225,28 @@ pub fn save_edits(wizard: &mut WrappedWizard, ui: &mut UI) -> Option<()> {
Some(l)
}
}),
)?)
} else {
None
)?;
if abstutil::file_exists(abstutil::path_edits(map.get_name(), &candidate)) {
let overwrite = "Overwrite";
let rename = "Rename";
if wizard
.choose_string(&format!("Edits named {} already exist", candidate), || {
vec![overwrite, rename]
})?
.as_str()
== overwrite
{
break candidate;
}
} else {
break candidate;
}
};
// TODO Do it this weird way to avoid saving edits on every event. :P
// TODO Do some kind of versioning? Don't ask this if the file doesn't exist yet?
let save = "save edits";
let cancel = "cancel";
if wizard
.choose_string("Overwrite edits?", || vec![save, cancel])?
.as_str()
== save
{
if let Some(name) = rename {
let mut edits = map.get_edits().clone();
edits.edits_name = name;
map.apply_edits(edits, &mut Timer::new("name map edits"));
}
map.save_edits();
}
let mut edits = map.get_edits().clone();
edits.edits_name = name;
map.apply_edits(edits, &mut Timer::new("name map edits"));
map.save_edits();
Some(())
}
@ -240,7 +254,9 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box<dyn State> {
WizardState::new(Box::new(move |wiz, ctx, ui| {
let mut wizard = wiz.wrap(ctx);
if ui.primary.map.get_edits().dirty {
if ui.primary.map.get_edits().edits_name == "no_edits"
&& !ui.primary.map.get_edits().commands.is_empty()
{
let save = "save edits";
let discard = "discard";
if wizard
@ -248,13 +264,14 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box<dyn State> {
.as_str()
== save
{
save_edits(&mut wizard, ui)?;
save_edits_as(&mut wizard, ui)?;
wizard.reset();
}
}
// TODO Exclude current
let map_name = ui.primary.map.get_name().to_string();
let current_edits_name = ui.primary.map.get_edits().edits_name.clone();
let map_name = ui.primary.map.get_name().clone();
let (_, new_edits) = wizard.choose_exact(
(
HorizontalAlignment::Centered(btn.center().x),
@ -265,7 +282,9 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box<dyn State> {
let mut list = Choice::from(
abstutil::load_all_objects(abstutil::path_all_edits(&map_name))
.into_iter()
.filter(|(_, edits)| mode.allows(edits))
.filter(|(_, edits)| {
mode.allows(edits) && edits.edits_name != current_edits_name
})
.collect(),
);
list.push(Choice::new("no_edits", MapEdits::new(map_name.clone())));
@ -273,7 +292,6 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box<dyn State> {
},
)?;
apply_map_edits(ctx, ui, new_edits);
ui.primary.map.mark_edits_fresh();
Some(Transition::Pop)
}))
}
@ -301,6 +319,13 @@ fn make_topcenter(ctx: &mut EventCtx, ui: &UI) -> Composite {
"load edits",
)
.margin(5),
WrappedComposite::svg_button(
ctx,
"assets/tools/save.svg",
"save edits as",
lctrl(Key::S),
)
.margin(5),
(if !ui.primary.map.get_edits().commands.is_empty() {
WrappedComposite::svg_button(
ctx,
@ -316,9 +341,10 @@ fn make_topcenter(ctx: &mut EventCtx, ui: &UI) -> Composite {
)
})
.margin(15),
]),
WrappedComposite::text_button(ctx, "save edits", lctrl(Key::S)),
WrappedComposite::text_button(ctx, "finish editing", hotkey(Key::Escape)),
])
.centered(),
WrappedComposite::text_button(ctx, "finish editing", hotkey(Key::Escape))
.centered_horiz(),
])
.bg(colors::PANEL_BG),
)
@ -326,8 +352,7 @@ fn make_topcenter(ctx: &mut EventCtx, ui: &UI) -> Composite {
.build(ctx)
}
pub fn apply_map_edits(ctx: &mut EventCtx, ui: &mut UI, mut edits: MapEdits) {
edits.dirty = true;
pub fn apply_map_edits(ctx: &mut EventCtx, ui: &mut UI, edits: MapEdits) {
let mut timer = Timer::new("apply map edits");
let (lanes_changed, roads_changed, turns_deleted, turns_added, mut modified_intersections) =

View File

@ -6,7 +6,7 @@ use crate::colors;
use crate::common::{tool_panel, CommonState, Minimap, Overlays, ShowBusRoute};
use crate::debug::DebugMode;
use crate::edit::{
apply_map_edits, can_edit_lane, save_edits, EditMode, LaneEditor, StopSignEditor,
apply_map_edits, can_edit_lane, save_edits_as, EditMode, LaneEditor, StopSignEditor,
TrafficSignalEditor,
};
use crate::game::{DrawBaselayer, State, Transition, WizardState};
@ -319,11 +319,12 @@ impl State for SandboxMode {
fn exit_sandbox(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
let mut wizard = wiz.wrap(ctx);
let dirty = ui.primary.map.get_edits().dirty;
let unsaved = ui.primary.map.get_edits().edits_name == "no_edits"
&& !ui.primary.map.get_edits().commands.is_empty();
let (resp, _) = wizard.choose("Sure you want to abandon the current challenge?", || {
let mut choices = Vec::new();
choices.push(Choice::new("keep playing", ()));
if dirty {
if unsaved {
choices.push(Choice::new("save edits and quit", ()));
}
choices.push(Choice::new("quit challenge", ()).key(Key::Q));
@ -334,12 +335,13 @@ fn exit_sandbox(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Tra
}
let map_name = ui.primary.map.get_name().to_string();
if resp == "save edits and quit" {
save_edits(&mut wizard, ui)?;
save_edits_as(&mut wizard, ui)?;
}
ctx.loading_screen("reset map and sim", |ctx, mut timer| {
if !ui.primary.map.get_edits().is_empty() {
if ui.primary.map.get_edits().edits_name != "no_edits"
|| !ui.primary.map.get_edits().commands.is_empty()
{
apply_map_edits(ctx, ui, MapEdits::new(map_name));
ui.primary.map.mark_edits_fresh();
ui.primary
.map
.recalculate_pathfinding_after_edits(&mut timer);

View File

@ -19,9 +19,6 @@ pub struct MapEdits {
// Edits without these are player generated.
pub proposal_description: Vec<String>,
#[serde(skip_serializing, skip_deserializing)]
pub dirty: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -65,14 +62,9 @@ impl MapEdits {
original_lts: BTreeMap::new(),
reversed_lanes: BTreeSet::new(),
changed_intersections: BTreeSet::new(),
dirty: false,
}
}
pub fn is_empty(&self) -> bool {
self.edits_name == "no_edits" && self.commands.is_empty()
}
pub fn load(map_name: &str, edits_name: &str, timer: &mut Timer) -> MapEdits {
if edits_name == "no_edits" {
return MapEdits::new(map_name.to_string());
@ -80,14 +72,12 @@ impl MapEdits {
abstutil::read_json(abstutil::path_edits(map_name, edits_name), timer)
}
// TODO Version these
// TODO Version these? Or it's unnecessary, since we have a command stack.
pub(crate) fn save(&mut self, map: &Map) {
self.compress(map);
assert!(self.dirty);
assert_ne!(self.edits_name, "no_edits");
abstutil::write_json(abstutil::path_edits(&self.map_name, &self.edits_name), self);
self.dirty = false;
}
pub fn original_it(&self, i: IntersectionID) -> IntersectionType {

View File

@ -630,11 +630,6 @@ impl Map {
&self.edits
}
pub fn mark_edits_fresh(&mut self) {
assert!(self.edits.dirty);
self.edits.dirty = false;
}
pub fn save_edits(&mut self) {
let mut edits = std::mem::replace(&mut self.edits, MapEdits::new(self.name.clone()));
edits.save(self);

View File

@ -72,7 +72,6 @@ impl SimFlags {
MapEdits::load(map.get_name(), &sim.edits_name, timer),
timer,
);
map.mark_edits_fresh();
map.recalculate_pathfinding_after_edits(timer);
}
sim.restore_paths(&map, timer);