egui_charts/ui/dialogs/
storage_conflict.rs1use crate::Chart;
5use crate::localization as l10n;
6use crate::services::ConflictChoice;
7use crate::theme::ThemeExt;
8use crate::ui_kit::buttons::Button;
9use crate::ui_kit::dialog::DialogFooter;
10use crate::ui_kit::modal::Modal;
11
12pub struct StorageConflictDialog {
32 title: String,
33 message: String,
34}
35
36impl StorageConflictDialog {
37 pub fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
40 Self {
41 title: title.into(),
42 message: message.into(),
43 }
44 }
45
46 pub fn show(self, ctx: &egui::Context) -> Option<ConflictChoice> {
49 let mut overwrite = false;
50 let response = Modal::new(("storage_conflict", &self.title), &self.title)
51 .closable(false)
52 .show(ctx, &mut true, |ui| {
53 ui.label(l10n::text(ui.ctx(), &self.message));
54 ui.add_space(ui.active_theme().spacing.md);
55 for choice in [
56 "Keep both saves your changes as a copy, then loads the other version.",
57 "Reload loads the other version and discards your changes.",
58 "Overwrite replaces the other version with your changes.",
59 ] {
60 ui.label(l10n::text(ui.ctx(), choice));
61 }
62 DialogFooter::new("Keep both")
63 .secondary("Reload")
64 .show_with_left(ui, |ui| {
65 overwrite = Button::new("Overwrite").ghost().show(ui).clicked();
66 })
67 })?;
68 match response.inner {
69 (true, _) => Some(ConflictChoice::KeepLocalCopy),
70 (_, true) => Some(ConflictChoice::ReloadRemote),
71 _ => overwrite.then_some(ConflictChoice::Retry),
72 }
73 }
74}
75
76#[derive(Debug, Default)]
93pub struct LocalCopiesDialog {
94 open: bool,
95 symbol: String,
97 discarding: Option<String>,
99}
100
101impl LocalCopiesDialog {
102 pub fn open(&mut self, chart: &mut Chart) {
104 self.open = true;
105 self.discarding = None;
106 self.symbol = chart.symbol().to_owned();
107 chart.list_local_copies();
108 }
109
110 pub fn is_open(&self) -> bool {
112 self.open
113 }
114
115 pub fn show(&mut self, ctx: &egui::Context, chart: &mut Chart) {
118 if !self.open {
119 return;
120 }
121 let symbol = chart.symbol().to_owned();
122 if symbol != self.symbol {
124 self.discarding = None;
125 self.symbol.clone_from(&symbol);
126 chart.list_local_copies();
127 }
128 let copies = chart.local_copies().map(<[String]>::to_vec);
129 let (mut load, mut discard) = (None, None);
130 let discarding = &mut self.discarding;
131 Modal::new("local_copies", "Kept copies of drawings")
132 .width(440.0)
133 .show(ctx, &mut self.open, |ui| {
134 let values = [("symbol", symbol.clone())];
135 ui.label(l10n::format_text(
136 ui.ctx(),
137 "Copies of the {symbol} drawings kept when another window or device changed \
138 them. Loading one replaces the drawings on the chart; Undo brings them back.",
139 &values,
140 ));
141 ui.add_space(ui.active_theme().spacing.md);
142 let Some(copies) = &copies else {
143 ui.label(l10n::text(ui.ctx(), "Looking for kept copies…"));
144 return;
145 };
146 if copies.is_empty() {
147 let text = "No copies of the {symbol} drawings are kept.";
148 ui.label(l10n::format_text(ui.ctx(), text, &values));
149 }
150 egui::ScrollArea::vertical()
151 .max_height(ui.available_height())
152 .show(ui, |ui| {
153 for name in copies {
154 ui.add(egui::Label::new(name).truncate())
155 .on_hover_text(name);
156 if discarding.as_deref() == Some(name.as_str()) {
157 let text = "Discard this copy? This cannot be undone.";
158 ui.label(l10n::text(ui.ctx(), text));
159 ui.horizontal(|ui| {
160 if Button::new("Discard").danger().show(ui).clicked() {
161 discard = Some(name.clone());
162 }
163 if Button::new("Cancel").ghost().show(ui).clicked() {
164 *discarding = None;
165 }
166 });
167 } else {
168 ui.horizontal(|ui| {
169 if Button::new("Load").secondary().show(ui).clicked() {
170 load = Some(name.clone());
171 }
172 if Button::new("Discard").ghost().show(ui).clicked() {
173 *discarding = Some(name.clone());
174 }
175 });
176 }
177 }
178 });
179 });
180 if let Some(name) = discard {
181 self.discarding = None;
182 chart.discard_local_copy(&name);
183 }
184 if let Some(name) = load {
185 self.open = false;
186 chart.load_local_copy(&name);
187 }
188 if !self.open {
189 self.discarding = None;
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::drawings::persistence::DrawingStorage;
198 use crate::model::{Bar, BarData};
199 use crate::services::document::tests::Revised;
200 use chrono::{TimeZone, Utc};
201 use egui_kittest::{Harness, kittest::Queryable};
202 use std::sync::Arc;
203
204 #[test]
207 fn a_kept_copy_is_discarded_only_once_confirmed() {
208 let host = Arc::new(Revised::default());
209 let empty = DrawingStorage::new("A").to_json().unwrap();
210 for name in ["A", "A (local copy)", "A (local copy 2)"] {
211 host.put(name, &empty);
212 }
213 let bar = Bar::new(Utc.timestamp_opt(0, 0).unwrap(), 10.0, 11.0, 9.0, 10.0, 1.0);
214 let mut chart = Chart::new(BarData::from_bars(vec![bar]));
215 chart.set_symbol("A");
216 chart.set_storage(host.clone());
217 let mut dialog = LocalCopiesDialog::default();
218 dialog.open(&mut chart);
219 assert!(chart.flush_stored_drawings());
220 let mut h = Harness::builder()
221 .with_context_setup(|ctx| {
222 crate::theme::apply_to_egui(ctx, &crate::theme::Theme::dark());
223 egui_extras::install_image_loaders(ctx);
224 })
225 .build_ui_state(
226 |ui, state: &mut (LocalCopiesDialog, Chart)| state.0.show(ui.ctx(), &mut state.1),
227 (dialog, chart),
228 );
229 h.run();
230 for confirmed in [false, true] {
232 h.query_all_by_label("Discard").next().unwrap().click();
233 h.run();
234 assert!(h.state_mut().1.flush_stored_drawings());
235 assert_eq!(host.get("A (local copy)").is_none(), confirmed);
236 }
237 h.run_steps(2);
239 h.get_by_label("Load").click();
240 h.run();
241 assert!(!h.state().0.is_open());
242 }
243}