ltn/save/
proposals_ui.rs

1use abstutil::Timer;
2use map_gui::tools::{FilePicker, FileSaver, FileSaverContents};
3use widgetry::tools::{ChooseSomething, PopupMsg};
4use widgetry::{lctrl, Choice, EventCtx, Key, MultiKey, State, Widget};
5
6use super::save_dialog::SaveDialog;
7use super::share::ShareProposal;
8use super::{PreserveState, Proposal, Proposals};
9use crate::{App, Transition};
10
11impl Proposals {
12    pub fn to_widget_expanded(&self, ctx: &EventCtx) -> Widget {
13        let mut col = Vec::new();
14        for (action, icon, hotkey) in [
15            ("New", "pencil", None),
16            ("Load", "folder", None),
17            ("Save", "save", Some(MultiKey::from(lctrl(Key::S)))),
18            ("Share", "share", None),
19            ("Export GeoJSON", "export", None),
20        ] {
21            col.push(
22                ctx.style()
23                    .btn_plain
24                    .icon_text(&format!("system/assets/tools/{icon}.svg"), action)
25                    .hotkey(hotkey)
26                    .build_def(ctx),
27            );
28        }
29
30        for (idx, proposal) in self.list.iter().enumerate() {
31            let button = ctx
32                .style()
33                .btn_solid_primary
34                .text(if idx == 0 {
35                    "1 - existing LTNs".to_string()
36                } else {
37                    format!("{} - {}", idx + 1, proposal.edits.edits_name)
38                })
39                .hotkey(Key::NUM_KEYS[idx])
40                .disabled(idx == self.current)
41                .build_widget(ctx, &format!("switch to proposal {}", idx));
42            col.push(Widget::row(vec![
43                button,
44                // The first proposal (usually "existing LTNs", unless we're in a special consultation
45                // mode) is special and can't ever be removed
46                if idx != 0 {
47                    ctx.style()
48                        .btn_close()
49                        .disabled(self.list.len() == 1)
50                        .build_widget(ctx, &format!("hide proposal {}", idx))
51                } else {
52                    Widget::nothing()
53                },
54            ]));
55            // If somebody tries to load too many proposals, just stop
56            if idx == 9 {
57                break;
58            }
59        }
60        Widget::col(col)
61    }
62
63    pub fn to_widget_collapsed(&self, ctx: &EventCtx) -> Widget {
64        let mut col = Vec::new();
65        for (action, icon) in [
66            ("New", "pencil"),
67            ("Load", "folder"),
68            ("Save", "save"),
69            ("Share", "share"),
70            ("Export GeoJSON", "export"),
71        ] {
72            col.push(
73                ctx.style()
74                    .btn_plain
75                    .icon(&format!("system/assets/tools/{icon}.svg"))
76                    .build_widget(ctx, action),
77            );
78        }
79        Widget::col(col)
80    }
81
82    pub fn handle_action(
83        ctx: &mut EventCtx,
84        app: &mut App,
85        preserve_state: &PreserveState,
86        action: &str,
87    ) -> Option<Transition> {
88        match action {
89            "New" => {
90                // Fork a new proposal from the first one
91                if app.per_map.proposals.current != 0 {
92                    switch_to_existing_proposal(ctx, app, 0);
93                }
94            }
95            "Load" => {
96                return Some(Transition::Push(load_picker_ui(
97                    ctx,
98                    app,
99                    preserve_state.clone(),
100                )));
101            }
102            "Save" => {
103                return Some(Transition::Push(SaveDialog::new_state(
104                    ctx,
105                    app,
106                    preserve_state.clone(),
107                )));
108            }
109            "Share" => {
110                return Some(Transition::Push(ShareProposal::new_state(ctx, app)));
111            }
112            "Export GeoJSON" => {
113                return Some(Transition::Push(match crate::export::geojson_string(app) {
114                    Ok(contents) => FileSaver::with_default_messages(
115                        ctx,
116                        format!("ltn_{}.geojson", app.per_map.map.get_name().map),
117                        super::start_dir(),
118                        FileSaverContents::String(contents),
119                    ),
120                    Err(err) => PopupMsg::new_state(ctx, "Export failed", vec![err.to_string()]),
121                }));
122            }
123            _ => {
124                if let Some(x) = action.strip_prefix("switch to proposal ") {
125                    let idx = x.parse::<usize>().unwrap();
126                    switch_to_existing_proposal(ctx, app, idx);
127                } else if let Some(x) = action.strip_prefix("hide proposal ") {
128                    let idx = x.parse::<usize>().unwrap();
129                    if idx == app.per_map.proposals.current {
130                        // First make sure we're not hiding the current proposal
131                        switch_to_existing_proposal(ctx, app, if idx == 0 { 1 } else { idx - 1 });
132                    }
133
134                    // Remove it
135                    app.per_map.proposals.list.remove(idx);
136
137                    // Fix up indices
138                    if idx < app.per_map.proposals.current {
139                        app.per_map.proposals.current -= 1;
140                    }
141                } else {
142                    return None;
143                }
144            }
145        }
146
147        Some(preserve_state.clone().switch_to_state(ctx, app))
148    }
149}
150
151fn switch_to_existing_proposal(ctx: &mut EventCtx, app: &mut App, idx: usize) {
152    app.per_map.proposals.current = idx;
153    app.per_map.map.must_apply_edits(
154        app.per_map.proposals.get_current().edits.clone(),
155        &mut Timer::throwaway(),
156    );
157    crate::redraw_all_icons(ctx, app);
158}
159
160fn load_picker_ui(
161    ctx: &mut EventCtx,
162    app: &App,
163    preserve_state: PreserveState,
164) -> Box<dyn State<App>> {
165    // Don't bother trying to filter out proposals currently loaded -- by loading twice, somebody
166    // effectively makes a copy to modify a bit
167    ChooseSomething::new_state(
168        ctx,
169        "Load which proposal?",
170        // basename (and thus list_all_objects) turn "foo.json.gz" into "foo.json", so further
171        // strip out the extension.
172        // TODO Fix basename, but make sure nothing downstream breaks
173        {
174            let mut choices = vec!["Load from file on your computer".to_string()];
175            choices.extend(
176                abstio::list_all_objects(abstio::path_all_ltn_proposals(
177                    app.per_map.map.get_name(),
178                ))
179                .into_iter()
180                .map(abstutil::basename),
181            );
182            Choice::strings(choices)
183        },
184        Box::new(move |name, ctx, app| {
185            if name == "Load from file on your computer" {
186                Transition::Replace(FilePicker::new_state(
187                    ctx,
188                    super::start_dir(),
189                    Box::new(move |ctx, app, maybe_file| {
190                        match maybe_file {
191                            Ok(Some((path, bytes))) => {
192                                match Proposal::load_from_bytes(ctx, app, &path, Ok(bytes)) {
193                                    Some(err_state) => Transition::Replace(err_state),
194                                    None => preserve_state.switch_to_state(ctx, app),
195                                }
196                            }
197                            // No file chosen, just quit the picker
198                            Ok(None) => Transition::Pop,
199                            Err(err) => Transition::Replace(PopupMsg::new_state(
200                                ctx,
201                                "Error",
202                                vec![err.to_string()],
203                            )),
204                        }
205                    }),
206                ))
207            } else {
208                match Proposal::load_from_path(
209                    ctx,
210                    app,
211                    abstio::path_ltn_proposals(app.per_map.map.get_name(), &name),
212                ) {
213                    Some(err_state) => Transition::Replace(err_state),
214                    None => preserve_state.switch_to_state(ctx, app),
215                }
216            }
217        }),
218    )
219}