ltn/save/
save_dialog.rs

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                    // TODO If we're clobbering something that exists in Proposals especially...
129                    // watch out
130                    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                    // TODO Maybe "Save as" should copy the proposal
134
135                    match inner_save(app) {
136                        // If we changed the name, we'll want to recreate the panel
137                        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}