1use anyhow::Result;
2
3use map_gui::tools::{FileSaver, FileSaverContents};
4use widgetry::tools::PopupMsg;
5use widgetry::{
6 DrawBaselayer, EventCtx, GfxCtx, Key, Line, MultiKey, Outcome, Panel, State, TextBox, Widget,
7};
8
9use super::PreserveState;
10use crate::{App, Transition};
11
12pub struct SaveDialog {
13 panel: Panel,
14 preserve_state: PreserveState,
15 can_overwrite: bool,
16}
17
18impl SaveDialog {
19 pub fn new_state(
20 ctx: &mut EventCtx,
21 app: &App,
22 preserve_state: PreserveState,
23 ) -> Box<dyn State<App>> {
24 let can_overwrite = app.per_map.proposals.current != 0;
25
26 let mut state = Self {
27 panel: Panel::new_builder(Widget::col(vec![
28 Widget::row(vec![
29 Line("Save proposal").small_heading().into_widget(ctx),
30 ctx.style().btn_close_widget(ctx),
31 ]),
32 ctx.style()
33 .btn_solid_primary
34 .text(if cfg!(target_arch = "wasm32") {
35 "Download as file"
36 } else {
37 "Save as file in other folder"
38 })
39 .build_widget(ctx, "save as file"),
40 Widget::horiz_separator(ctx, 1.0),
41 if cfg!(target_arch = "wasm32") {
42 Line("Save in your browser's local storage")
43 .small()
44 .into_widget(ctx)
45 } else {
46 Line("Save as a file in the A/B Street data folder")
47 .small()
48 .into_widget(ctx)
49 },
50 if can_overwrite {
51 Widget::row(vec![
52 ctx.style()
53 .btn_solid_destructive
54 .text(format!(
55 "Overwrite \"{}\"",
56 app.per_map.proposals.get_current().edits.edits_name
57 ))
58 .build_widget(ctx, "Overwrite"),
59 Line("Or save a new copy below")
60 .secondary()
61 .into_widget(ctx)
62 .centered_vert(),
63 ])
64 } else {
65 Widget::nothing()
66 },
67 Widget::row(vec![
68 TextBox::default_widget(ctx, "input", String::new()),
69 Widget::placeholder(ctx, "Save as"),
70 ]),
71 Widget::placeholder(ctx, "warning"),
72 ]))
73 .build(ctx),
74 preserve_state,
75 can_overwrite,
76 };
77 state.name_updated(ctx);
78 Box::new(state)
79 }
80
81 fn name_updated(&mut self, ctx: &mut EventCtx) {
82 let name = self.panel.text_box("input");
83
84 let warning = if name == "existing LTNs" {
85 Some("You can't overwrite the name \"existing LTNs\"")
86 } else if name.is_empty() {
87 Some("You have to name this proposal")
88 } else {
89 None
90 };
91
92 let btn = ctx
93 .style()
94 .btn_solid_primary
95 .text("Save as")
96 .disabled(warning.is_some())
97 .hotkey(if self.can_overwrite {
98 None
99 } else {
100 Some(MultiKey::from(Key::Enter))
101 })
102 .build_def(ctx);
103 self.panel.replace(ctx, "Save as", btn);
104
105 if let Some(warning) = warning {
106 self.panel
107 .replace(ctx, "warning", Line(warning).into_widget(ctx));
108 } else {
109 self.panel
110 .replace(ctx, "warning", Widget::placeholder(ctx, "warning"));
111 }
112 }
113
114 fn error(&self, ctx: &mut EventCtx, app: &mut App, err: impl AsRef<str>) -> Transition {
115 Transition::Multi(vec![
116 self.preserve_state.switch_to_state(ctx, app),
117 Transition::Push(PopupMsg::new_state(ctx, "Error", vec![err])),
118 ])
119 }
120}
121
122impl State<App> for SaveDialog {
123 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
124 match self.panel.event(ctx) {
125 Outcome::Clicked(x) => match x.as_ref() {
126 "close" => Transition::Pop,
127 "Save as" | "Overwrite" => {
128 let mut edits = app.per_map.map.get_edits().clone();
131 edits.edits_name = self.panel.text_box("input");
132 app.apply_edits(edits);
133 match inner_save(app) {
136 Ok(()) => self.preserve_state.switch_to_state(ctx, app),
138 Err(err) => {
139 self.error(ctx, app, format!("Couldn't save proposal: {}", err))
140 }
141 }
142 }
143 "save as file" => {
144 let proposal = app.per_map.proposals.get_current();
145 Transition::Replace(match proposal.to_gzipped_bytes(app) {
146 Ok(contents) => FileSaver::with_default_messages(
147 ctx,
148 format!("{}.json.gz", proposal.edits.edits_name),
149 super::start_dir(),
150 FileSaverContents::Bytes(contents),
151 ),
152 Err(err) => PopupMsg::new_state(ctx, "Save failed", vec![err.to_string()]),
153 })
154 }
155 _ => unreachable!(),
156 },
157 Outcome::Changed(_) => {
158 self.name_updated(ctx);
159 Transition::Keep
160 }
161 _ => {
162 if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
163 return Transition::Pop;
164 }
165 Transition::Keep
166 }
167 }
168 }
169
170 fn draw_baselayer(&self) -> DrawBaselayer {
171 DrawBaselayer::PreviousState
172 }
173
174 fn draw(&self, g: &mut GfxCtx, app: &App) {
175 map_gui::tools::grey_out_map(g, app);
176 self.panel.draw(g);
177 }
178}
179
180fn inner_save(app: &App) -> Result<()> {
181 let proposal = app.per_map.proposals.get_current();
182 let path = abstio::path_ltn_proposals(app.per_map.map.get_name(), &proposal.edits.edits_name);
183 let output_buffer = proposal.to_gzipped_bytes(app)?;
184 abstio::write_raw(path, &output_buffer)
185}