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