1 mod bulk;
2 mod cluster_traffic_signals;
3 mod lanes;
4 mod routes;
5 mod select;
6 mod stop_signs;
7 mod traffic_signals;
8 mod validate;
9 mod zones;
10
11 pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor;
12 pub use self::lanes::LaneEditor;
13 pub use self::routes::RouteEditor;
14 pub use self::stop_signs::StopSignEditor;
15 pub use self::traffic_signals::TrafficSignalEditor;
16 pub use self::validate::{
17 check_blackholes, check_sidewalk_connectivity, try_change_lt, try_reverse,
18 };
19 use crate::app::{App, ShowEverything};
20 use crate::common::{tool_panel, CommonState, Warping};
21 use crate::debug::DebugMode;
22 use crate::game::{PopupMsg, State, Transition};
23 use crate::helpers::ID;
24 use crate::options::OptionsPanel;
25 use crate::render::DrawMap;
26 use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen};
27 use abstutil::Timer;
28 use geom::Speed;
29 use map_model::{EditCmd, IntersectionID, LaneID, LaneType, MapEdits};
30 use maplit::btreeset;
31 use sim::DontDrawAgents;
32 use std::collections::BTreeSet;
33 use widgetry::{
34 hotkey, lctrl, Btn, Choice, Color, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Key, Line,
35 Menu, Outcome, Panel, PersistentSplit, RewriteColor, Text, TextExt, VerticalAlignment, Widget,
36 };
37
38 pub struct EditMode {
39 tool_panel: Panel,
40 top_center: Panel,
41 changelist: Panel,
42 orig_edits: MapEdits,
43 orig_dirty: bool,
44
45 // Retained state from the SandboxMode that spawned us
46 mode: GameplayMode,
47
48 // edits name, number of commands
49 changelist_key: (String, usize),
50
51 unzoomed: Drawable,
52 zoomed: Drawable,
53 }
54
55 impl EditMode {
new(ctx: &mut EventCtx, app: &mut App, mode: GameplayMode) -> Box<dyn State>56 pub fn new(ctx: &mut EventCtx, app: &mut App, mode: GameplayMode) -> Box<dyn State> {
57 let orig_dirty = app.primary.dirty_from_edits;
58 assert!(app.suspended_sim.is_none());
59 app.suspended_sim = Some(app.primary.clear_sim());
60 let edits = app.primary.map.get_edits();
61 let layer = crate::layer::map::Static::edits(ctx, app);
62 Box::new(EditMode {
63 tool_panel: tool_panel(ctx),
64 top_center: make_topcenter(ctx, app, &mode),
65 changelist: make_changelist(ctx, app),
66 orig_edits: edits.clone(),
67 orig_dirty,
68 mode,
69 changelist_key: (edits.edits_name.clone(), edits.commands.len()),
70 unzoomed: layer.unzoomed,
71 zoomed: layer.zoomed,
72 })
73 }
74
quit(&self, ctx: &mut EventCtx, app: &mut App) -> Transition75 fn quit(&self, ctx: &mut EventCtx, app: &mut App) -> Transition {
76 let old_sim = app.suspended_sim.take().unwrap();
77
78 // If nothing changed, short-circuit
79 if app.primary.map.get_edits() == &self.orig_edits {
80 app.primary.sim = old_sim;
81 app.primary.dirty_from_edits = self.orig_dirty;
82 // Could happen if we load some edits, then load whatever we entered edit mode with.
83 ctx.loading_screen("apply edits", |_, mut timer| {
84 app.primary
85 .map
86 .recalculate_pathfinding_after_edits(&mut timer);
87 });
88 return Transition::Pop;
89 }
90
91 ctx.loading_screen("apply edits", move |ctx, mut timer| {
92 app.primary
93 .map
94 .recalculate_pathfinding_after_edits(&mut timer);
95 // Parking state might've changed
96 app.primary.clear_sim();
97 if app.opts.resume_after_edit {
98 if self.mode.reset_after_edits() {
99 Transition::Multi(vec![
100 Transition::Pop,
101 Transition::Replace(SandboxMode::new(ctx, app, self.mode.clone())),
102 Transition::Push(TimeWarpScreen::new(ctx, app, old_sim.time(), None)),
103 ])
104 } else {
105 app.primary.sim = old_sim;
106 app.primary.dirty_from_edits = true;
107 app.primary
108 .sim
109 .handle_live_edited_traffic_signals(&app.primary.map);
110 Transition::Pop
111 }
112 } else {
113 Transition::Multi(vec![
114 Transition::Pop,
115 Transition::Replace(SandboxMode::new(ctx, app, self.mode.clone())),
116 ])
117 }
118 })
119 }
120 }
121
122 impl State for EditMode {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition123 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
124 {
125 let edits = app.primary.map.get_edits();
126 let changelist_key = (edits.edits_name.clone(), edits.commands.len());
127 if self.changelist_key != changelist_key {
128 self.changelist_key = changelist_key;
129 self.changelist = make_changelist(ctx, app);
130 let layer = crate::layer::map::Static::edits(ctx, app);
131 self.unzoomed = layer.unzoomed;
132 self.zoomed = layer.zoomed;
133 }
134 }
135
136 ctx.canvas_movement();
137 // Restrict what can be selected.
138 if ctx.redo_mouseover() {
139 app.primary.current_selection = app.calculate_current_selection(
140 ctx,
141 &DontDrawAgents {},
142 &ShowEverything::new(),
143 false,
144 true,
145 false,
146 );
147 if let Some(ID::Lane(l)) = app.primary.current_selection {
148 if !can_edit_lane(&self.mode, l, app) {
149 app.primary.current_selection = None;
150 }
151 } else if let Some(ID::Intersection(i)) = app.primary.current_selection {
152 if app.primary.map.maybe_get_stop_sign(i).is_some()
153 && !self.mode.can_edit_stop_signs()
154 {
155 app.primary.current_selection = None;
156 }
157 } else if let Some(ID::Road(_)) = app.primary.current_selection {
158 } else {
159 app.primary.current_selection = None;
160 }
161 }
162
163 if app.opts.dev && ctx.input.pressed(lctrl(Key::D)) {
164 return Transition::Push(Box::new(DebugMode::new(ctx)));
165 }
166
167 match self.top_center.event(ctx) {
168 Outcome::Clicked(x) => match x.as_ref() {
169 "bulk edit" => {
170 return Transition::Push(bulk::BulkSelect::new(ctx, app));
171 }
172 "finish editing" => {
173 return self.quit(ctx, app);
174 }
175 _ => unreachable!(),
176 },
177 _ => {}
178 }
179 match self.changelist.event(ctx) {
180 Outcome::Clicked(x) => match x.as_ref() {
181 "load edits" => {
182 if app.primary.map.unsaved_edits() {
183 return Transition::Multi(vec![
184 Transition::Push(LoadEdits::new(ctx, app, self.mode.clone())),
185 Transition::Push(SaveEdits::new(
186 ctx,
187 app,
188 "Do you want to save your edits first?",
189 true,
190 Some(Transition::Multi(vec![Transition::Pop, Transition::Pop])),
191 )),
192 ]);
193 } else {
194 return Transition::Push(LoadEdits::new(ctx, app, self.mode.clone()));
195 }
196 }
197 "save edits as" | "save edits" => {
198 return Transition::Push(SaveEdits::new(
199 ctx,
200 app,
201 "Save your edits",
202 false,
203 Some(Transition::Pop),
204 ));
205 }
206 "undo" => {
207 let mut edits = app.primary.map.get_edits().clone();
208 let maybe_id = cmd_to_id(&edits.commands.pop().unwrap());
209 apply_map_edits(ctx, app, edits);
210 if let Some(id) = maybe_id {
211 return Transition::Push(Warping::new(
212 ctx,
213 id.canonical_point(&app.primary).unwrap(),
214 Some(10.0),
215 Some(id),
216 &mut app.primary,
217 ));
218 }
219 }
220 x => {
221 let idx = x["most recent change #".len()..].parse::<usize>().unwrap();
222 if let Some(id) = cmd_to_id(
223 &app.primary.map.get_edits().commands
224 [app.primary.map.get_edits().commands.len() - idx],
225 ) {
226 return Transition::Push(Warping::new(
227 ctx,
228 id.canonical_point(&app.primary).unwrap(),
229 Some(10.0),
230 Some(id),
231 &mut app.primary,
232 ));
233 }
234 }
235 },
236 _ => {}
237 }
238 // Just kind of constantly scrape this
239 app.opts.resume_after_edit = self.top_center.persistent_split_value("finish editing");
240
241 if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
242 if let Some(id) = &app.primary.current_selection {
243 if app.per_obj.left_click(ctx, "edit this") {
244 return Transition::Push(Warping::new(
245 ctx,
246 id.canonical_point(&app.primary).unwrap(),
247 Some(10.0),
248 None,
249 &mut app.primary,
250 ));
251 }
252 }
253 } else {
254 if let Some(ID::Intersection(id)) = app.primary.current_selection {
255 if let Some(state) = maybe_edit_intersection(ctx, app, id, &self.mode) {
256 return Transition::Push(state);
257 }
258 }
259 if let Some(ID::Lane(l)) = app.primary.current_selection {
260 if app.per_obj.left_click(ctx, "edit lane") {
261 return Transition::Push(LaneEditor::new(ctx, app, l, self.mode.clone()));
262 }
263 }
264 }
265
266 match self.tool_panel.event(ctx) {
267 Outcome::Clicked(x) => match x.as_ref() {
268 "back" => self.quit(ctx, app),
269 "settings" => Transition::Push(OptionsPanel::new(ctx, app)),
270 _ => unreachable!(),
271 },
272 _ => Transition::Keep,
273 }
274 }
275
draw(&self, g: &mut GfxCtx, app: &App)276 fn draw(&self, g: &mut GfxCtx, app: &App) {
277 self.tool_panel.draw(g);
278 self.top_center.draw(g);
279 self.changelist.draw(g);
280 if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
281 g.redraw(&self.unzoomed);
282 } else {
283 g.redraw(&self.zoomed);
284 }
285 CommonState::draw_osd(g, app);
286 }
287 }
288
289 pub struct SaveEdits {
290 panel: Panel,
291 current_name: String,
292 cancel: Option<Transition>,
293 reset: bool,
294 }
295
296 impl SaveEdits {
new( ctx: &mut EventCtx, app: &App, title: &str, discard: bool, cancel: Option<Transition>, ) -> Box<dyn State>297 pub fn new(
298 ctx: &mut EventCtx,
299 app: &App,
300 title: &str,
301 discard: bool,
302 cancel: Option<Transition>,
303 ) -> Box<dyn State> {
304 let initial_name = if app.primary.map.unsaved_edits() {
305 String::new()
306 } else {
307 format!("copy of {}", app.primary.map.get_edits().edits_name)
308 };
309 let btn = SaveEdits::btn(ctx, app, &initial_name);
310 Box::new(SaveEdits {
311 current_name: initial_name.clone(),
312 panel: Panel::new(Widget::col(vec![
313 Line(title).small_heading().draw(ctx),
314 Widget::row(vec![
315 "Name:".draw_text(ctx),
316 Widget::text_entry(ctx, initial_name, true).named("filename"),
317 ]),
318 Widget::row(vec![
319 btn,
320 if discard {
321 Btn::text_bg2("Discard edits").build_def(ctx, None)
322 } else {
323 Widget::nothing()
324 },
325 if cancel.is_some() {
326 Btn::text_bg2("Cancel").build_def(ctx, hotkey(Key::Escape))
327 } else {
328 Widget::nothing()
329 },
330 ]),
331 ]))
332 .build(ctx),
333 cancel,
334 reset: discard,
335 })
336 }
337
btn(ctx: &mut EventCtx, app: &App, candidate: &str) -> Widget338 fn btn(ctx: &mut EventCtx, app: &App, candidate: &str) -> Widget {
339 if candidate.is_empty() {
340 Btn::text_bg2("Save").inactive(ctx)
341 } else if abstutil::file_exists(abstutil::path_edits(app.primary.map.get_name(), candidate))
342 {
343 Btn::text_bg2("Overwrite existing edits").build_def(ctx, None)
344 } else {
345 Btn::text_bg2("Save").build_def(ctx, hotkey(Key::Enter))
346 }
347 .named("save")
348 }
349 }
350
351 impl State for SaveEdits {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition352 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
353 match self.panel.event(ctx) {
354 Outcome::Clicked(x) => match x.as_ref() {
355 "Save" | "Overwrite existing edits" => {
356 let mut edits = app.primary.map.get_edits().clone();
357 edits.edits_name = self.current_name.clone();
358 app.primary
359 .map
360 .must_apply_edits(edits, &mut Timer::new("name map edits"));
361 app.primary.map.save_edits();
362 if self.reset {
363 apply_map_edits(ctx, app, MapEdits::new());
364 }
365 return Transition::Pop;
366 }
367 "Discard edits" => {
368 apply_map_edits(ctx, app, MapEdits::new());
369 return Transition::Pop;
370 }
371 "Cancel" => {
372 return self.cancel.take().unwrap();
373 }
374 _ => unreachable!(),
375 },
376 _ => {}
377 }
378 let name = self.panel.text_box("filename");
379 if name != self.current_name {
380 self.current_name = name;
381 let btn = SaveEdits::btn(ctx, app, &self.current_name);
382 self.panel.replace(ctx, "save", btn);
383 }
384
385 Transition::Keep
386 }
387
draw(&self, g: &mut GfxCtx, app: &App)388 fn draw(&self, g: &mut GfxCtx, app: &App) {
389 State::grey_out_map(g, app);
390 self.panel.draw(g);
391 }
392 }
393
394 struct LoadEdits {
395 panel: Panel,
396 mode: GameplayMode,
397 }
398
399 impl LoadEdits {
new(ctx: &mut EventCtx, app: &App, mode: GameplayMode) -> Box<dyn State>400 fn new(ctx: &mut EventCtx, app: &App, mode: GameplayMode) -> Box<dyn State> {
401 let current_edits_name = &app.primary.map.get_edits().edits_name;
402 let your_edits = vec![
403 Line("Your edits").small_heading().draw(ctx),
404 Menu::new(
405 ctx,
406 abstutil::list_all_objects(abstutil::path_all_edits(app.primary.map.get_name()))
407 .into_iter()
408 .map(|name| Choice::new(name.clone(), ()).active(&name != current_edits_name))
409 .collect(),
410 ),
411 ];
412 // widgetry can't toggle keyboard focus between two menus, so just use buttons for the less
413 // common use case.
414 let mut proposals = vec![Line("Community proposals").small_heading().draw(ctx)];
415 // Up-front filter out proposals that definitely don't fit the current map
416 for name in abstutil::list_all_objects(abstutil::path("system/proposals")) {
417 let path = abstutil::path(format!("system/proposals/{}.json", name));
418 if MapEdits::load(&app.primary.map, path.clone(), &mut Timer::throwaway()).is_ok() {
419 proposals.push(Btn::text_fg(&name).build(ctx, path, None));
420 }
421 }
422
423 Box::new(LoadEdits {
424 mode,
425 panel: Panel::new(Widget::col(vec![
426 Widget::row(vec![
427 Line("Load edits").small_heading().draw(ctx),
428 Btn::plaintext("X")
429 .build(ctx, "close", hotkey(Key::Escape))
430 .align_right(),
431 ]),
432 Btn::text_fg("Start over with blank edits").build_def(ctx, None),
433 Widget::row(vec![Widget::col(your_edits), Widget::col(proposals)]).evenly_spaced(),
434 ]))
435 .exact_size_percent(50, 50)
436 .build(ctx),
437 })
438 }
439 }
440
441 impl State for LoadEdits {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition442 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
443 match self.panel.event(ctx) {
444 Outcome::Clicked(x) => {
445 match x.as_ref() {
446 "close" => Transition::Pop,
447 "Start over with blank edits" => {
448 apply_map_edits(ctx, app, MapEdits::new());
449 Transition::Pop
450 }
451 path => {
452 // TODO Kind of a hack. If it ends with .json, it's already a path.
453 // Otherwise it's a result from the menu.
454 let path = if path.ends_with(".json") {
455 path.to_string()
456 } else {
457 abstutil::path_edits(app.primary.map.get_name(), path)
458 };
459
460 match MapEdits::load(
461 &app.primary.map,
462 path.clone(),
463 &mut Timer::throwaway(),
464 )
465 .and_then(|edits| {
466 if self.mode.allows(&edits) {
467 Ok(edits)
468 } else {
469 Err(
470 "The current gameplay mode restricts edits. These edits have \
471 a banned command."
472 .to_string(),
473 )
474 }
475 }) {
476 Ok(edits) => {
477 apply_map_edits(ctx, app, edits);
478 Transition::Pop
479 }
480 // TODO Hack. Have to replace ourselves, because the Menu might be
481 // invalidated now that something was chosen.
482 Err(err) => {
483 println!("Can't load {}: {}", path, err);
484 Transition::Multi(vec![
485 Transition::Replace(LoadEdits::new(
486 ctx,
487 app,
488 self.mode.clone(),
489 )),
490 // TODO Menu draws at a weird Z-order to deal with tooltips, so
491 // now the menu underneath
492 // bleeds through
493 Transition::Push(PopupMsg::new(
494 ctx,
495 "Error",
496 vec![format!("Can't load {}", path), err.clone()],
497 )),
498 ])
499 }
500 }
501 }
502 }
503 }
504 _ => Transition::Keep,
505 }
506 }
507
draw(&self, g: &mut GfxCtx, app: &App)508 fn draw(&self, g: &mut GfxCtx, app: &App) {
509 State::grey_out_map(g, app);
510 self.panel.draw(g);
511 }
512 }
513
make_topcenter(ctx: &mut EventCtx, app: &App, mode: &GameplayMode) -> Panel514 fn make_topcenter(ctx: &mut EventCtx, app: &App, mode: &GameplayMode) -> Panel {
515 Panel::new(Widget::col(vec![
516 Line("Editing map")
517 .small_heading()
518 .draw(ctx)
519 .centered_horiz(),
520 Widget::row(vec![
521 if mode.can_edit_lanes() {
522 Btn::text_fg("bulk edit").build_def(ctx, hotkey(Key::B))
523 } else {
524 Btn::text_fg("bulk edit").inactive(ctx)
525 },
526 PersistentSplit::new(
527 ctx,
528 "finish editing",
529 app.opts.resume_after_edit,
530 hotkey(Key::Escape),
531 vec![
532 Choice::new(
533 format!(
534 "Finish & resume from {}",
535 app.suspended_sim.as_ref().unwrap().time().ampm_tostring()
536 ),
537 true,
538 ),
539 Choice::new("Finish & restart from midnight", false),
540 ],
541 )
542 .bg(app.cs.section_bg),
543 ]),
544 ]))
545 .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
546 .build(ctx)
547 }
548
apply_map_edits(ctx: &mut EventCtx, app: &mut App, edits: MapEdits)549 pub fn apply_map_edits(ctx: &mut EventCtx, app: &mut App, edits: MapEdits) {
550 let mut timer = Timer::new("apply map edits");
551
552 let (roads_changed, turns_deleted, turns_added, mut modified_intersections) =
553 app.primary.map.must_apply_edits(edits, &mut timer);
554
555 if !roads_changed.is_empty() || !modified_intersections.is_empty() {
556 app.primary
557 .draw_map
558 .draw_all_unzoomed_roads_and_intersections =
559 DrawMap::regenerate_unzoomed_layer(&app.primary.map, &app.cs, ctx, &mut timer);
560 }
561
562 for r in roads_changed {
563 let road = app.primary.map.get_r(r);
564 app.primary.draw_map.roads[r.0].clear_rendering();
565
566 // An edit to one lane potentially affects markings in all lanes in the same road, because
567 // of one-way markings, driving lines, etc.
568 for l in road.all_lanes() {
569 app.primary.draw_map.lanes[l.0].clear_rendering();
570 }
571 }
572
573 let mut lanes_of_modified_turns: BTreeSet<LaneID> = BTreeSet::new();
574 for t in turns_deleted {
575 lanes_of_modified_turns.insert(t.src);
576 modified_intersections.insert(t.parent);
577 }
578 for t in &turns_added {
579 lanes_of_modified_turns.insert(t.src);
580 modified_intersections.insert(t.parent);
581 }
582
583 for i in modified_intersections {
584 app.primary.draw_map.intersections[i.0].clear_rendering();
585 }
586
587 if app.layer.as_ref().and_then(|l| l.name()) == Some("map edits") {
588 app.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app)));
589 }
590
591 // Autosave
592 if app.primary.map.get_edits().edits_name != "untitled edits" {
593 app.primary.map.save_edits();
594 }
595 }
596
can_edit_lane(mode: &GameplayMode, l: LaneID, app: &App) -> bool597 pub fn can_edit_lane(mode: &GameplayMode, l: LaneID, app: &App) -> bool {
598 mode.can_edit_lanes()
599 && !app.primary.map.get_l(l).is_walkable()
600 && app.primary.map.get_l(l).lane_type != LaneType::SharedLeftTurn
601 && !app.primary.map.get_l(l).is_light_rail()
602 }
603
change_speed_limit(ctx: &mut EventCtx, default: Speed) -> Widget604 pub fn change_speed_limit(ctx: &mut EventCtx, default: Speed) -> Widget {
605 let mut choices = vec![
606 Choice::new("10 mph", Speed::miles_per_hour(10.0)),
607 Choice::new("15 mph", Speed::miles_per_hour(15.0)),
608 Choice::new("20 mph", Speed::miles_per_hour(20.0)),
609 Choice::new("25 mph", Speed::miles_per_hour(25.0)),
610 Choice::new("30 mph", Speed::miles_per_hour(30.0)),
611 Choice::new("35 mph", Speed::miles_per_hour(35.0)),
612 Choice::new("40 mph", Speed::miles_per_hour(40.0)),
613 Choice::new("45 mph", Speed::miles_per_hour(45.0)),
614 Choice::new("50 mph", Speed::miles_per_hour(50.0)),
615 Choice::new("55 mph", Speed::miles_per_hour(55.0)),
616 Choice::new("60 mph", Speed::miles_per_hour(60.0)),
617 Choice::new("65 mph", Speed::miles_per_hour(65.0)),
618 Choice::new("70 mph", Speed::miles_per_hour(70.0)),
619 // Don't need anything higher. Though now I kind of miss 3am drives on TX-71...
620 ];
621 if !choices.iter().any(|c| c.data == default) {
622 choices.push(Choice::new(default.to_string(), default));
623 }
624
625 Widget::row(vec![
626 "Change speed limit:".draw_text(ctx).centered_vert(),
627 Widget::dropdown(ctx, "speed limit", default, choices),
628 ])
629 }
630
maybe_edit_intersection( ctx: &mut EventCtx, app: &mut App, id: IntersectionID, mode: &GameplayMode, ) -> Option<Box<dyn State>>631 pub fn maybe_edit_intersection(
632 ctx: &mut EventCtx,
633 app: &mut App,
634 id: IntersectionID,
635 mode: &GameplayMode,
636 ) -> Option<Box<dyn State>> {
637 if app.primary.map.maybe_get_stop_sign(id).is_some()
638 && mode.can_edit_stop_signs()
639 && app.per_obj.left_click(ctx, "edit stop signs")
640 {
641 return Some(StopSignEditor::new(ctx, app, id, mode.clone()));
642 }
643
644 if app.primary.map.maybe_get_traffic_signal(id).is_some()
645 && app.per_obj.left_click(ctx, "edit traffic signal")
646 {
647 return Some(TrafficSignalEditor::new(
648 ctx,
649 app,
650 btreeset! {id},
651 mode.clone(),
652 ));
653 }
654
655 if app.primary.map.get_i(id).is_closed()
656 && app.per_obj.left_click(ctx, "re-open closed intersection")
657 {
658 // This resets to the original state; it doesn't undo the closure to the last
659 // state. Seems reasonable to me.
660 let mut edits = app.primary.map.get_edits().clone();
661 edits.commands.push(EditCmd::ChangeIntersection {
662 i: id,
663 old: app.primary.map.get_i_edit(id),
664 new: edits.original_intersections[&id].clone(),
665 });
666 apply_map_edits(ctx, app, edits);
667 }
668
669 None
670 }
671
make_changelist(ctx: &mut EventCtx, app: &App) -> Panel672 fn make_changelist(ctx: &mut EventCtx, app: &App) -> Panel {
673 // TODO Support redo. Bit harder here to reset the redo_stack when the edits
674 // change, because nested other places modify it too.
675 let edits = app.primary.map.get_edits();
676 let mut col = vec![
677 Widget::row(vec![
678 Btn::text_fg(format!("{} ↓", &edits.edits_name)).build(
679 ctx,
680 "load edits",
681 lctrl(Key::L),
682 ),
683 (if edits.commands.is_empty() {
684 Widget::draw_svg_transform(
685 ctx,
686 "system/assets/tools/save.svg",
687 RewriteColor::ChangeAll(Color::WHITE.alpha(0.5)),
688 )
689 } else {
690 Btn::svg_def("system/assets/tools/save.svg").build(
691 ctx,
692 "save edits as",
693 lctrl(Key::S),
694 )
695 })
696 .centered_vert(),
697 (if !edits.commands.is_empty() {
698 Btn::svg_def("system/assets/tools/undo.svg").build(ctx, "undo", lctrl(Key::Z))
699 } else {
700 Widget::draw_svg_transform(
701 ctx,
702 "system/assets/tools/undo.svg",
703 RewriteColor::ChangeAll(Color::WHITE.alpha(0.5)),
704 )
705 })
706 .centered_vert(),
707 ]),
708 if app.primary.map.unsaved_edits() {
709 Btn::text_fg("Unsaved edits").build(ctx, "save edits", None)
710 } else {
711 Btn::text_fg("Autosaved!").inactive(ctx)
712 },
713 Text::from_multiline(vec![
714 Line(format!("{} roads changed", edits.changed_roads.len())),
715 Line(format!(
716 "{} intersections changed",
717 edits.original_intersections.len()
718 )),
719 ])
720 .draw(ctx),
721 ];
722
723 for (idx, cmd) in edits.commands.iter().rev().take(5).enumerate() {
724 col.push(
725 Btn::plaintext(format!("{}) {}", idx + 1, cmd.short_name(&app.primary.map))).build(
726 ctx,
727 format!("most recent change #{}", idx + 1),
728 None,
729 ),
730 );
731 }
732 if edits.commands.len() > 5 {
733 col.push(format!("{} more...", edits.commands.len()).draw_text(ctx));
734 }
735
736 Panel::new(Widget::col(col))
737 .aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
738 .build(ctx)
739 }
740
741 // TODO Ideally a Tab.
cmd_to_id(cmd: &EditCmd) -> Option<ID>742 fn cmd_to_id(cmd: &EditCmd) -> Option<ID> {
743 match cmd {
744 EditCmd::ChangeRoad { r, .. } => Some(ID::Road(*r)),
745 EditCmd::ChangeIntersection { i, .. } => Some(ID::Intersection(*i)),
746 EditCmd::ChangeRouteSchedule { .. } => None,
747 }
748 }
749