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 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 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 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 switch_to_existing_proposal(ctx, app, if idx == 0 { 1 } else { idx - 1 });
132 }
133
134 app.per_map.proposals.list.remove(idx);
136
137 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 ChooseSomething::new_state(
168 ctx,
169 "Load which proposal?",
170 {
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 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}