1use crate::config::KeyboardAction;
6use crate::icons::icons as embedded_icons;
7use crate::localization as l10n;
8use crate::model::{Alert, AlertComparison, AlertCondition};
9use crate::theme::Theme;
10use crate::ui::context_menu::{MenuEntry, MenuItem, show_menu};
11use crate::widget::{Chart, PaneId};
12
13use egui::{Context, Id, Pos2};
14
15pub(crate) const DRAWING_MENU_OFFSET: f32 = 6.0;
19
20pub(crate) fn move_targets(
23 ctx: Option<&Context>,
24 chart: &Chart,
25 drawing_id: usize,
26) -> Vec<(PaneId, String)> {
27 let current = chart.drawing_pane(drawing_id);
28 let mut panes = super::study::other_panes(ctx, chart, current);
29 if current.is_some_and(|pane| pane != PaneId::MAIN) {
30 panes.push((
31 PaneId::MAIN,
32 ctx.map_or_else(
33 || "Main pane".to_owned(),
34 |ctx| l10n::text(ctx, "Main pane"),
35 ),
36 ));
37 }
38 panes
39}
40
41#[derive(Debug, Clone, PartialEq)]
43#[non_exhaustive]
44pub enum DrawingContextMenuAction {
45 None,
47 AddAlert(Box<Alert>),
52 Edit,
54 BringToFront,
56 BringForward,
58 SendToBack,
60 SendBackward,
62 Copy,
64 MoveTo(PaneId),
66 Lock,
68 Hide,
70 Clone,
72 Delete,
74}
75
76#[derive(Clone, Debug)]
78pub struct DrawingContextMenu {
79 is_open: bool,
80 position: Pos2,
81 pub drawing_id: Option<usize>,
83 pub drawing_type: String,
85 pub is_locked: bool,
87 pub is_visible: bool,
89 pub alert: Option<Alert>,
91 pub panes: Vec<(PaneId, String)>,
93 pub last_action: DrawingContextMenuAction,
95 just_opened: bool,
97}
98
99impl Default for DrawingContextMenu {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl DrawingContextMenu {
106 pub fn new() -> Self {
108 Self {
109 is_open: false,
110 position: Pos2::ZERO,
111 drawing_id: None,
112 drawing_type: "Drawing".to_string(),
113 is_locked: false,
114 is_visible: true,
115 alert: None,
116 panes: Vec::new(),
117 last_action: DrawingContextMenuAction::None,
118 just_opened: false,
119 }
120 }
121
122 pub fn open(&mut self, pos: Pos2, drawing_id: usize) {
124 self.is_open = true;
125 self.position = pos;
126 self.drawing_id = Some(drawing_id);
127 self.last_action = DrawingContextMenuAction::None;
128 self.just_opened = true;
129 }
130
131 pub fn set_context(&mut self, drawing_type: impl Into<String>, locked: bool, visible: bool) {
133 self.drawing_type = drawing_type.into();
134 self.is_locked = locked;
135 self.is_visible = visible;
136 }
137
138 pub(crate) fn open_on(&mut self, ctx: &Context, chart: &Chart, pos: Pos2, drawing_id: usize) {
141 let drawing = chart
142 .drawings()
143 .drawings
144 .iter()
145 .find(|d| d.id == drawing_id);
146 if let Some(drawing) = drawing {
147 self.set_context(drawing.tool_type.as_str(), drawing.locked, drawing.visible);
148 }
149 self.alert = drawing
150 .filter(|_| !chart.symbol.is_empty())
151 .and_then(|drawing| {
152 let (level, _, _) = drawing
153 .alert_levels_with_bars(
154 chart
155 .bars(crate::chart::series::SeriesId::MAIN)
156 .len()
157 .saturating_sub(1) as f64,
158 chart.config().right_axis_scale_mode,
159 chart.bars(crate::chart::series::SeriesId::MAIN),
160 )
161 .into_iter()
162 .next()?;
163 Some(Alert::new(
164 chart.symbol.clone(),
165 AlertCondition::Compare {
166 other: drawing.alert_source(level),
167 comparison: AlertComparison::Crossing,
168 },
169 ))
170 });
171 self.panes = move_targets(Some(ctx), chart, drawing_id);
172 self.open(pos, drawing_id);
173 }
174
175 pub fn close(&mut self) {
177 self.is_open = false;
178 }
179
180 pub fn is_open(&self) -> bool {
182 self.is_open
183 }
184
185 pub fn take_action(&mut self) -> DrawingContextMenuAction {
187 std::mem::replace(&mut self.last_action, DrawingContextMenuAction::None)
188 }
189
190 pub(crate) fn build_menu_items(
192 &self,
193 ctx: &Context,
194 shortcuts: &crate::widget::ShortcutMap,
195 ) -> Vec<MenuEntry<DrawingContextMenuAction>> {
196 let alert = self.alert.iter().flat_map(|alert| {
197 [
198 MenuEntry::Item(
199 MenuItem::new(
200 l10n::format_text(
201 ctx,
202 "Add alert on {name}…",
203 &[("name", l10n::text(ctx, &self.drawing_type).as_str())],
204 ),
205 DrawingContextMenuAction::AddAlert(Box::new(alert.clone())),
206 )
207 .with_icon(&embedded_icons::ALERTS),
208 ),
209 MenuEntry::Separator,
210 ]
211 });
212 alert
213 .chain([
214 MenuEntry::Item(
216 MenuItem::new("Edit…", DrawingContextMenuAction::Edit)
217 .with_icon(&embedded_icons::PENCIL),
218 ),
219 MenuEntry::Separator,
220 MenuEntry::Item(MenuItem::new(
222 "Bring to front",
223 DrawingContextMenuAction::BringToFront,
224 )),
225 MenuEntry::Item(MenuItem::new(
226 "Bring forward",
227 DrawingContextMenuAction::BringForward,
228 )),
229 MenuEntry::Item(MenuItem::new(
230 "Send to back",
231 DrawingContextMenuAction::SendToBack,
232 )),
233 MenuEntry::Item(MenuItem::new(
234 "Send backward",
235 DrawingContextMenuAction::SendBackward,
236 )),
237 ])
238 .chain((!self.panes.is_empty()).then(|| {
239 MenuEntry::Submenu {
240 label: "Move to".into(),
241 icon: Some(&embedded_icons::MOVE_PANE),
242 entries: self
243 .panes
244 .iter()
245 .map(|(pane, title)| {
246 MenuEntry::Item(MenuItem::new(
247 title,
248 DrawingContextMenuAction::MoveTo(*pane),
249 ))
250 })
251 .collect(),
252 }
253 }))
254 .chain([
255 MenuEntry::Separator,
256 MenuEntry::Item(
258 MenuItem::new(
259 if self.is_locked { "Unlock" } else { "Lock" },
260 DrawingContextMenuAction::Lock,
261 )
262 .with_icon(if self.is_locked {
263 &embedded_icons::UNLOCK
264 } else {
265 &embedded_icons::LOCK
266 }),
267 ),
268 MenuEntry::Item(
269 MenuItem::new(
270 if self.is_visible { "Hide" } else { "Show" },
271 DrawingContextMenuAction::Hide,
272 )
273 .with_icon(if self.is_visible {
274 &embedded_icons::EYE_HIDE
275 } else {
276 &embedded_icons::EYE
277 }),
278 ),
279 MenuEntry::Separator,
280 MenuEntry::Item(
281 MenuItem::new("Copy", DrawingContextMenuAction::Copy)
282 .with_shortcut(shortcuts.shortcut(KeyboardAction::CopyDrawing)),
283 ),
284 MenuEntry::Item(MenuItem::new("Clone", DrawingContextMenuAction::Clone)),
285 MenuEntry::Separator,
286 MenuEntry::Item(
288 MenuItem::new("Delete", DrawingContextMenuAction::Delete)
289 .with_icon(&embedded_icons::TRASH)
290 .with_shortcut(shortcuts.shortcut(KeyboardAction::DeleteSelectedDrawing))
291 .danger(),
292 ),
293 ])
294 .collect()
295 }
296
297 pub fn show(&mut self, ctx: &Context, theme: &Theme) {
299 self.show_with_shortcuts(ctx, theme, &crate::widget::ShortcutMap::default());
300 }
301
302 pub fn show_with_shortcuts(
304 &mut self,
305 ctx: &Context,
306 theme: &Theme,
307 shortcuts: &crate::widget::ShortcutMap,
308 ) {
309 if !self.is_open {
310 return;
311 }
312 let entries = self.build_menu_items(ctx, shortcuts);
313 let just_opened = std::mem::take(&mut self.just_opened);
314 let id = Id::unique("drawing_context_menu");
315 if let Some(action) = show_menu(
316 ctx,
317 id,
318 theme,
319 self.position,
320 DRAWING_MENU_OFFSET,
321 &entries,
322 just_opened,
323 ) {
324 self.last_action = action.unwrap_or(DrawingContextMenuAction::None);
325 self.close();
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::drawings::{ChartPoint, Drawing, DrawingToolType, FibonacciConfig};
334 use crate::model::{AlertSource, BarData};
335
336 #[test]
338 fn alerts_start_on_a_visible_drawing_level() {
339 let mut chart = Chart::new(BarData::default());
340 chart.symbol = "SAMPLE".into();
341 let mut drawing = Drawing::new(7, DrawingToolType::FibonacciRetracement);
342 drawing.chart_points = vec![ChartPoint::new(0.0, 100.0), ChartPoint::new(10.0, 200.0)];
343 drawing.stored_id = Some("fib".into());
344 chart.drawings_mut().drawings.push(drawing);
345 let mut menu = DrawingContextMenu::new();
346 for visible in [vec![0, 1], vec![1], vec![]] {
347 let mut config = FibonacciConfig::default();
348 for (index, level) in config.levels.iter_mut().enumerate() {
349 level.visible = visible.contains(&index);
350 }
351 chart.drawings_mut().drawings[0].fib_config = Some(config);
352 menu.open_on(&Context::default(), &chart, Pos2::ZERO, 7);
353 assert_eq!(
354 menu.alert.as_ref().map(|alert| &alert.condition),
355 visible
356 .first()
357 .map(|level| AlertCondition::Compare {
358 other: AlertSource::Drawing(if *level == 0 {
359 "fib".into()
360 } else {
361 "fib#1".into()
362 }),
363 comparison: AlertComparison::Crossing,
364 })
365 .as_ref()
366 );
367 }
368 }
369}